// SPDX-License-Identifier: GPL-2.0

#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt

#include <linux/string.h>
#include <linux/err.h>
#include <linux/vmalloc.h>
#include <linux/slab.h>
#include <linux/rculist.h>
#include <crypto/hash.h>
#include <linux/mutex.h>

#include "dim_core_measure_log.h"
#include "../common/dim_rb_tree.h"
#include "../adapter/dim_adapter.h"
#include "../common/dim_hash.h"
#include "../common/dim_utils.h"

/* it can be changed and the max is 254 */
#define MEASURE_LOG_COUNTS_LIMIT_FOR_ONE_FILE 10

struct measure_log_forest {
	dim_rb_handle rb_list;
	struct list_head node;
};

enum dim_measure_log_flag {
	DIM_NG_LOG_FLAG,
	DIM_LOG_FLAG_LAST
};

struct list_head trees_root;
struct list_head ordered_list;

static struct measure_log_forest *cur_tree;

static dim_rb_handle counts_rb_list;

static char *dim_flag_array[DIM_LOG_FLAG_LAST] = {
	"dim-ng"
};

struct dim_measure_log_node {
	char pcr;
	char log_flag;
	char hash_index;
	char reserved;
	char *digest;
	char *temp_digest;
	char *path;
	struct list_head node;
};

struct dim_measure_log_count_node {
	char *path;
	unsigned long counts;
};

static int measure_log_compare(void *data1, void *data2)
{
	int ret;
	struct dim_measure_log_node *d1 =
		(struct dim_measure_log_node *)data1;
	struct dim_measure_log_node *d2 =
		(struct dim_measure_log_node *)data2;

	if (d1 == NULL || d2 == NULL)
		return 0;

	ret = strcmp(d1->path, d2->path);
	if (ret != 0)
		return ret;

	if (d1->hash_index != d2->hash_index)
		return (int)d1->hash_index - d2->hash_index;

	return memcmp(d1->digest, d2->digest,
		dim_hash_get_digest_len(d1->hash_index));
}

static int measure_log_count_node_compare(void *data1, void *data2)
{
	struct dim_measure_log_count_node *d1 =
		(struct dim_measure_log_count_node *)data1;
	struct dim_measure_log_count_node *d2 =
		(struct dim_measure_log_count_node *)data2;

	if (d1 == NULL || d2 == NULL)
		return 0;

	return strcmp(d1->path, d2->path);
}

int dim_measure_log_init(void)
{
	INIT_LIST_HEAD_RCU(&trees_root);
	INIT_LIST_HEAD_RCU(&ordered_list);

	counts_rb_list = dim_rb_create(measure_log_count_node_compare);
	if (counts_rb_list == NULL)
		pr_warn("not enough memory to alloc counts tree\n");

	return 0;
}

void dim_measure_log_uninit(void)
{
	struct dim_measure_log_node *data = NULL;
	struct measure_log_forest *tree = NULL;
	struct dim_measure_log_count_node *count_node = NULL;

	for (tree = list_first_or_null_rcu(&trees_root,
		struct measure_log_forest, node);
	     tree != NULL;
	     tree = list_first_or_null_rcu(&trees_root,
		struct measure_log_forest, node)) {
		for (data = (struct dim_measure_log_node *)
			dim_rb_remove_first_data(tree->rb_list);
		     data != NULL;
		     data = (struct dim_measure_log_node *)
			dim_rb_remove_first_data(tree->rb_list)) {
			kfree(data->path);
			kfree(data->digest);
			kfree(data->temp_digest);
			kfree(data);
		}

		list_del_rcu(&tree->node);
		dim_rb_destroy(tree->rb_list);
		kfree(tree);
	}

	INIT_LIST_HEAD_RCU(&trees_root);
	INIT_LIST_HEAD_RCU(&ordered_list);

	if (counts_rb_list != NULL) {
		for (count_node = (struct dim_measure_log_count_node *)
			dim_rb_remove_first_data(counts_rb_list);
		     count_node != NULL;
		     count_node = (struct dim_measure_log_count_node *)
			dim_rb_remove_first_data(counts_rb_list)) {
			if (count_node->path != NULL)
				kfree(count_node->path);
			kfree(count_node);
		}

		counts_rb_list = NULL;
	}
}

static void counts_rb_list_zero(void)
{
	struct dim_measure_log_count_node *count_node = NULL;

	if (counts_rb_list == NULL)
		return;

	for (count_node = dim_rb_first_data(counts_rb_list);
	     count_node != NULL;
	     count_node = dim_rb_next_data(counts_rb_list))
		count_node->counts = 0;
}

void dim_measure_log_prepare_for_baseline(void)
{
	struct measure_log_forest *new_tree = NULL;

	if (cur_tree != NULL && dim_rb_first_data(cur_tree->rb_list) == NULL)
		return;

	new_tree = kmalloc(sizeof(*new_tree), GFP_KERNEL);
	if (new_tree == NULL) {
		pr_err("no memory to alloc structure in measure log\n");
		return;
	}

	new_tree->rb_list = dim_rb_create(measure_log_compare);
	if (new_tree->rb_list == NULL) {
		pr_err("no memory to alloc list in measure log\n");
		kfree(new_tree);
		return;
	}

	list_add_tail_rcu(&new_tree->node, &trees_root);
	cur_tree = new_tree;
	counts_rb_list_zero();
}

static bool find_measure_log(dim_rb_handle root,
			     char *path,
			     char *digest,
			     enum dim_hash_type hash_index)
{
	struct dim_measure_log_node node;

	node.path = path;
	node.digest = digest;
	node.hash_index = hash_index;

	return dim_rb_find_data(root, &node) == NULL ? false : true;
}

bool dim_measure_log_find_baseline(char *path,
				   char *digest,
				   enum dim_hash_type hash_index,
				   int search_all)
{
	struct measure_log_forest *tree = NULL;
	struct dim_measure_log_count_node *count_node = NULL;
	struct dim_measure_log_count_node count_tmp = {.path = path};

	/* check all log number of path */
	count_node = dim_rb_find_data(counts_rb_list, &count_tmp);
	if (count_node != NULL &&
	    count_node->counts >= MEASURE_LOG_COUNTS_LIMIT_FOR_ONE_FILE) {
		count_node->counts = MEASURE_LOG_COUNTS_LIMIT_FOR_ONE_FILE;
		return true;
	}

	if (!search_all) /* only search in current measure log tree */
		return find_measure_log(cur_tree->rb_list,
			path, digest, hash_index);

	/* search in total measure log */
	for (tree = list_first_or_null_rcu(&trees_root,
		struct measure_log_forest, node);
	     tree != NULL;
	     tree = list_next_or_null_rcu(&trees_root,
		&tree->node, struct measure_log_forest, node)) {
		if (find_measure_log(tree->rb_list, path,
			digest, hash_index))
			return true;
	}

	return false;
}

static int calc_template_digest(char *path,
				char *digest,
				enum dim_hash_type hash_index,
				char *digest_out)
{
	int ret, size;
	int len = dim_hash_get_digest_len(hash_index);
	struct crypto_shash *tfm = dim_hash_get_tfm(hash_index);

	DIM_SHASH_DESC_ON_STACK(shash);

	if (tfm == NULL)
		return -EOPNOTSUPP;

	shash->tfm = tfm;

	ret = crypto_shash_init(shash);
	if (ret < 0) {
		pr_warn("shash init failed: %d\n", ret);
		return ret;
	}

	/* templet hash is hash(
		"file hash algorithm string size + file digest size"
		+ "file hash algorithm string"
		+ "file digest"
		+ "file path string size"
		+ "file path"), and the "\0" of algorithm/path string should
		be calculated in also.

	for example, if the file is "/bin/ls", algorithm is "sha256", and
	file digest is
	"xxxx...xxx"
	then:
	"file hash algorithm string size" is: strlen("sha256:") + 1
	"file digest size" is: 32
	"file path string size" is: strlen("/bin/ls") + 1
	*/
	size = strlen(dim_hash_get_name_by_index(hash_index)) + strlen(":") + 1 + len;
	crypto_shash_update(shash, (char *)&size, sizeof(size));
	crypto_shash_update(shash, dim_hash_get_name_by_index(hash_index),
		strlen(dim_hash_get_name_by_index(hash_index)));
	crypto_shash_update(shash, ":", 2); /* 2 is the len of ":" and "\0" */
	crypto_shash_update(shash, digest, len);

	size = strlen(path) + 1;
	crypto_shash_update(shash, (char *)&size, sizeof(size));
	crypto_shash_update(shash, path, strlen(path) + 1); /* + "\0" */

	return crypto_shash_final(shash, digest_out);
}

static void add_files_record_counts(char *path)
{
	struct dim_measure_log_count_node *count_node = NULL;
	struct dim_measure_log_count_node count_tmp = {.path = path};

	count_node = dim_rb_find_data(counts_rb_list, &count_tmp);
	if (count_node == NULL) {
		count_node = kmalloc(sizeof(*count_node), GFP_KERNEL);
		if (count_node == NULL)
			return; /* not important code */

		count_node->path = kmalloc(strlen(path) + 1, GFP_KERNEL);
		if (count_node->path == NULL) {
			kfree(count_node);
			return;
		}
		strcpy(count_node->path, path);
		count_node->counts = 0;
		if (dim_rb_insert_data(counts_rb_list, count_node, NULL) < 0) {
			kfree(count_node->path);
			kfree(count_node);
			return;
		}
	}
	count_node->counts++;
}

int dim_write_measure_log(char *path, char *digest, enum dim_hash_type hash_index)
{
	struct dim_measure_log_node *new = NULL;
	struct dim_measure_log_node *find = NULL;
	int len = dim_hash_get_digest_len(hash_index);
	int ret = -ENOMEM;

	if (path == NULL || digest == NULL || len == 0)
		return -EINVAL;

	new = kzalloc(sizeof(struct dim_measure_log_node), GFP_KERNEL);
	if (new == NULL)
		return -ENOMEM;

	new->digest = kmalloc(len, GFP_KERNEL);
	if (new->digest == NULL)
		goto err;

	new->temp_digest = kmalloc(len, GFP_KERNEL);
	if (new->temp_digest == NULL)
		goto err;

	new->path = kmalloc(strlen(path) + 1, GFP_KERNEL);
	if (new->path == NULL)
		goto err;

	new->pcr = dim_get_extend_flag();
	new->log_flag = DIM_NG_LOG_FLAG;
	new->hash_index = hash_index;
	memcpy(new->digest, digest, len);
	strcpy(new->path, path);

	ret = dim_rb_insert_data(cur_tree->rb_list, new, (void **)&find);
	if (ret < 0) {
		if (ret == -EEXIST)
			pr_err("%s has been logged or logs exceed limits\n",
				path);
		goto err;
	}

	ret = calc_template_digest(path, digest, hash_index, new->temp_digest);
	if (ret < 0) {
		pr_err("fail to calculate tamplate digest: %d\n", ret);
		goto err;
	}

	list_add_tail_rcu(&new->node, &ordered_list);
	add_files_record_counts(path);
	return 0;

err:
	if (new->digest != NULL)
		kfree(new->digest);
	if (new->temp_digest != NULL)
		kfree(new->temp_digest);
	if (new->path != NULL)
		kfree(new->path);
	kfree(new);

	if (ret == -ENOMEM)
		pr_err("measure log write failed, no memory\n");

	return ret;
}

struct list_head *measure_log_read_cur;

struct list_head *dim_get_measure_log_first(void)
{
	struct dim_measure_log_node *first = list_first_or_null_rcu(&ordered_list,
								struct dim_measure_log_node, node);

	return first != NULL ? &first->node : NULL;
}

struct list_head *dim_get_measure_log_next(struct list_head *cur)
{
	struct dim_measure_log_node *next = list_next_or_null_rcu(&ordered_list,
					cur, struct dim_measure_log_node, node);

	return next != NULL ? &next->node : NULL;
}

char *dim_show_measure_log_by_node(struct list_head *cur, char *line_buf, size_t buf_len)
{
	struct dim_measure_log_node *node = NULL;
	int remain = buf_len - 1;
	size_t digest_len;
	size_t hex_len;

	if (cur == NULL || line_buf == NULL ||
	    buf_len == 0 || cur == &ordered_list)
		return NULL;

	node = list_entry_rcu(cur, struct dim_measure_log_node, node);

	if (remain < sizeof(int))
		return NULL;

	remain -= snprintf(line_buf + (buf_len - 1 - remain), remain, "%u ", node->pcr);

	digest_len = dim_hash_get_digest_len(dim_hash_get_cur_index());
	hex_len = (digest_len << 1);
	if (remain <= hex_len)
		return NULL;

	dim_hex_to_string(node->temp_digest, digest_len,
			line_buf + (buf_len - 1 - remain), remain);
	remain -= hex_len;
	*(line_buf + (buf_len - 1 - remain)) = ' ';
	remain--;

	if (remain <= strlen(dim_flag_array[(unsigned int)node->log_flag]))
		return NULL;

	remain -= snprintf(line_buf + (buf_len - 1 - remain), remain,
				"%s ",
				dim_flag_array[(unsigned int)node->log_flag]);

	if (remain <= strlen(dim_hash_get_name_by_index(node->hash_index)))
		return NULL;

	remain -= snprintf(line_buf + (buf_len - 1 - remain), remain,
				"%s:",
				dim_hash_get_name_by_index(node->hash_index));

	digest_len = dim_hash_get_digest_len(node->hash_index);
	hex_len = (digest_len << 1);
	if (remain <= hex_len)
		return NULL;

	dim_hex_to_string(node->digest, digest_len,
			line_buf + (buf_len - 1 - remain), remain);
	remain -= hex_len;
	*(line_buf + (buf_len - 1 - remain)) = ' ';
	remain--;

	if (remain < strlen(node->path))
		return NULL;

	remain -= snprintf(line_buf + (buf_len - 1 - remain), remain, "%s", node->path);
	*(line_buf + (buf_len - 1 - remain)) = 0;

	return line_buf;
}
